Passed
Pull Request — master (#136)
by
unknown
02:40
created

Frame.ts ➔ createFromBuffer   B

Complexity

Conditions 7

Size

Total Lines 40
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 33
dl 0
loc 40
rs 7.688
c 0
b 0
f 0
cc 7
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as Frames from './Frames'
8
import * as ID3Util from './ID3Util'
9
import { isKeyOf } from "./util"
10
11
type HeaderInfo = {
12
    identifier: string
13
    headerSize: number
14
    bodySize: number
15
    flags: Flags
16
}
17
18
export class Frame {
19
    identifier: string
20
    private value: unknown
21
    flags: Flags
22
23
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
24
        this.identifier = identifier
25
        this.value = value
26
        this.flags = flags
27
    }
28
29
    static createFromBuffer = createFromBuffer
30
31
    makeBuffer() {
32
        return makeFrameBuffer(this.identifier, this.value)
33
    }
34
35
    getValue() {
36
        return this.value
37
    }
38
}
39
40
function makeFrameBuffer(identifier: string, value: unknown) {
41
    if (isKeyOf(identifier, Frames.Frames)) {
42
        return Frames.Frames[identifier].create(value)
43
    }
44
    if (identifier.startsWith('T')) {
45
        return Frames.GENERIC_TEXT.create(identifier, value)
46
    }
47
    if (identifier.startsWith('W')) {
48
        return Frames.GENERIC_URL.create(identifier, value)
49
    }
50
    return null
51
}
52
53
function createFromBuffer(
54
    frameBuffer: Buffer,
55
    version: number
56
): Frame | null {
57
    const headerSize = getHeaderSize(version)
58
    // Specification requirement
59
    if (frameBuffer.length < headerSize + 1) {
60
        return null
61
    }
62
    const headerBuffer = frameBuffer.subarray(0, headerSize)
63
    const header: HeaderInfo = {
64
        headerSize,
65
        ...FrameHeader.createFromBuffer(headerBuffer, version)
66
    }
67
    if (header.flags.encryption) {
68
        return null
69
    }
70
71
    const body = decompressBody(
72
        header.flags,
73
        getDataLength(header, frameBuffer),
74
        getBody(header, frameBuffer)
75
    )
76
    if (!body) {
77
        return null
78
    }
79
80
    const identifier = header.identifier
81
    let value = null
82
    if (isKeyOf(identifier, Frames.Frames)) {
83
        value = Frames.Frames[identifier].read(body, version)
84
    } else if (identifier.startsWith('T')) {
85
        value = Frames.GENERIC_TEXT.read(body)
86
    } else if (identifier.startsWith('W')) {
87
        value = Frames.GENERIC_URL.read(body)
88
    } else {
89
        return null
90
    }
91
    return new Frame(identifier, value, header.flags)
92
}
93
94
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
95
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
96
    const bodyStart = headerSize + bodyOffset
97
    const bodyEnd = bodyStart + bodySize - bodyOffset
98
    const body = buffer.subarray(bodyStart, bodyEnd)
99
    if (flags.unsynchronisation) {
100
        // This method should stay in ID3Util for now because it's also used in the Tag's header which we don't have a class for.
101
        return ID3Util.processUnsynchronisedBuffer(body)
102
    }
103
    return body
104
}
105
106
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
107
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
108
}
109
110
function decompressBody(
111
    {compression}: Flags,
112
    dataLength: number,
113
    body: Buffer
114
) {
115
    return compression ? decompressBuffer(body, dataLength) : body
116
}
117
118
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
119
    if (buffer.length < 5) {
120
        return null
121
    }
122
123
    // ID3 spec defines that compression is stored in ZLIB format,
124
    // but doesn't specify if header is present or not.
125
    // ZLIB has a 2-byte header.
126
    // 1. try if header + body decompression
127
    // 2. else try if header is not stored (assume that all content is deflated "body")
128
    // 3. else try if inflation works if the header is omitted (implementation dependent)
129
    const tryDecompress = () => {
130
        try {
131
            return zlib.inflateSync(buffer)
132
        } catch (error) {
133
            try {
134
                return zlib.inflateRawSync(buffer)
135
            } catch (error) {
136
                try {
137
                    return zlib.inflateRawSync(buffer.subarray(2))
138
                } catch (error) {
139
                    return null
140
                }
141
            }
142
        }
143
    }
144
    const decompressed = tryDecompress()
145
    if (decompressed && decompressed.length === expectedDecompressedLength) {
146
        return decompressed
147
    }
148
    return null
149
}
150